import { expect } from 'chai';
import * as sinon from 'sinon';
import { updateStack } from '../src/updateStack';
import * as step from '@flow-step/step-toolkit';
import * as getParametersModule from '../src/getParameters';
import * as getTemplateModule from '../src/getTemplate';
import { UpdateStackRequest, UpdateStackResponse } from '@alicloud/ros20190910';

describe('updateStack', () => {
  let sandbox: sinon.SinonSandbox;
  let mockRos: any;
  let mockParams: any;
  let stepInfoStub: sinon.SinonStub;
  let stepErrorStub: sinon.SinonStub;
  let getParametersStub: sinon.SinonStub;
  let getTemplateBodyStub: sinon.SinonStub;

  beforeEach(() => {
    sandbox = sinon.createSandbox();
    mockRos = {
      updateStack: sandbox.stub()
    };
    mockParams = {
      STACK_NAME: 'test-stack',
      REGION_ID: 'cn-hangzhou',
      TIMEOUT_MINUTES: 60,
      RAM_ROLE_NAME: 'test-role',
      pipelineID: 123,
      pipelineName: 'test-pipeline',
      workSpace: '/test/workspace',
      projectDir: '/test/project',
      buildJobID: 456,
      ROS_DEPLOYMENT_LOCATION: 'cn',
      ROS_STACK_FILE_PATH: 'template.json'
    };
    stepInfoStub = sandbox.stub(step, 'info');
    stepErrorStub = sandbox.stub(step, 'error');
    getParametersStub = sandbox.stub(getParametersModule, 'getParameters');
    getTemplateBodyStub = sandbox.stub(getTemplateModule, 'getTemplateBody');
  });

  afterEach(() => {
    sandbox.restore();
  });

  it('should return stackId when update succeeds', async () => {
    const mockParameters = new Map([['key1', 'value1']]);
    const mockTemplate = '{"Resources": {"Test": {"Type": "ALIYUN::ECS::Instance"}}}';
    const mockResponse = {
      body: {
        stackId: 'stack-12345'
      }
    };

    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns(mockTemplate);
    mockRos.updateStack.resolves(mockResponse);

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.equal('stack-12345');
    expect(mockRos.updateStack.calledOnce).to.be.true;
  });

  it('should handle "no changes" error gracefully', async () => {
    const mockParameters = new Map();
    const mockTemplate = '{"Resources": {}}';
    const mockError = {
      statusCode: 400,
      data: {
        Message: 'Update the completely same stack'
      }
    };

    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns(mockTemplate);
    mockRos.updateStack.rejects(mockError);

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.equal('stack-12345');
    expect(stepInfoStub.calledWith('Stack test-stack has no changes. No update needed.')).to.be.true;
  });

  it('should handle "already updating" error gracefully', async () => {
    const mockParameters = new Map();
    const mockTemplate = '{"Resources": {}}';
    const mockError = {
      statusCode: 400,
      data: {
        Code: 'LastTokenProcessing',
        Message: 'Stack is already updating'
      }
    };

    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns(mockTemplate);
    mockRos.updateStack.rejects(mockError);

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.equal('stack-12345');
    expect(stepInfoStub.calledWith('Stack test-stack is already updating.')).to.be.true;
  });

  it('should handle other API errors', async () => {
    const mockParameters = new Map();
    const mockTemplate = '{"Resources": {}}';
    const mockError = {
      statusCode: 400,
      data: {
        Message: 'Invalid template format',
        RequestId: 'req-123'
      }
    };

    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns(mockTemplate);
    mockRos.updateStack.rejects(mockError);

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.be.false;
    expect(stepErrorStub.calledWith('Failed to UpdateStack. Code: undefined, Message: Invalid template format, RequestId: req-123')).to.be.true;
  });

  it('should handle unexpected errors without message', async () => {
    getParametersStub.returns(new Map());
    getTemplateBodyStub.returns('{"Resources": {}}');
    const error = new Error('Unexpected error');
    (error as any).statusCode = 500;
    (error as any).data = { Message: 'Unexpected error', RequestId: 'test-request-id' };
    mockRos.updateStack.rejects(error);

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.be.false;
    expect(stepErrorStub.calledWith('Failed to UpdateStack. Code: undefined, Message: Unexpected error, RequestId: test-request-id')).to.be.true;
  });

  it('should return false when stackId is empty', async () => {
    const mockParameters = new Map();
    const mockTemplate = '{"Resources": {}}';
    const mockResponse = {
      body: {
        stackId: null
      }
    };

    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns(mockTemplate);
    mockRos.updateStack.resolves(mockResponse);

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.be.false;
  });

  it('should construct correct UpdateStackRequest', async () => {
    const mockParameters = new Map([['key1', 'value1'], ['key2', 'value2']]);
    const mockTemplate = '{"Resources": {}}';
    
    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns(mockTemplate);
    mockRos.updateStack.resolves({ body: { stackId: 'stack-123' } });

    await updateStack(mockParams, mockRos, 'stack-12345');

    const call = mockRos.updateStack.getCall(0);
    const request = call.args[0];
    
    expect(request.regionId).to.equal('cn-hangzhou');
    expect(request.templateBody).to.equal(mockTemplate);
    expect(request.stackId).to.equal('stack-12345');
    expect(request.timeoutInMinutes).to.equal(60);
    expect(request.ramRoleName).to.equal('test-role');
    expect(request.parameters).to.have.length(2);
  });

  it('should update stack without optional parameters when not provided', async () => {
    const paramsWithoutOptional = {
      STACK_NAME: 'test-stack',
      REGION_ID: 'cn-hangzhou',
      pipelineID: 123,
      pipelineName: 'test-pipeline',
      workSpace: '/test/workspace',
      projectDir: '/test/project',
      buildJobID: 456,
      ROS_DEPLOYMENT_LOCATION: 'cn',
      RAM_ROLE_NAME: '',
      ROS_STACK_FILE_PATH: 'template.json',
      TIMEOUT_MINUTES: 0
    };
    const mockParameters = new Map();
    const mockTemplate = '{"Resources": {}}';
    
    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns(mockTemplate);
    mockRos.updateStack.resolves({ body: { stackId: 'stack-123' } });

    const result = await updateStack(paramsWithoutOptional, mockRos, 'stack-12345');

    expect(result).to.equal('stack-123');
    const call = mockRos.updateStack.getCall(0);
    const request = call.args[0];
    
    expect(request.timeoutInMinutes).to.equal(60);
    expect(request.ramRoleName).to.equal('AliyunROSFullRole');
    expect(request.parameters).to.deep.equal([]);
  });

  it('should handle stack with no changes', async () => {
    const mockParameters = new Map();
    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns('{"Resources": {}}');
    
    const error = new Error('Update the completely same stack');
    (error as any).statusCode = 400;
    (error as any).data = { Message: 'Update the completely same stack message', Code: 'StackNotChanged' };
    mockRos.updateStack.rejects(error);

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.equal('stack-12345');
    expect(stepInfoStub.calledWith('Stack test-stack has no changes. No update needed.')).to.be.true;
  });

  it('should handle stack already updating', async () => {
    const mockParameters = new Map();
    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns('{"Resources": {}}');
    
    const error = new Error('Last token processing');
    (error as any).statusCode = 400;
    (error as any).data = { Message: 'Last token processing', Code: 'LastTokenProcessing' };
    mockRos.updateStack.rejects(error);

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.equal('stack-12345');
    expect(stepInfoStub.calledWith('Stack test-stack is already updating.')).to.be.true;
  });

  it('should handle null stackId in response', async () => {
    getParametersStub.returns(new Map());
    getTemplateBodyStub.returns('{"Resources": {}}');
    mockRos.updateStack.resolves({ body: { stackId: null } });

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.be.false;
  });

  it('should handle undefined stackId in response', async () => {
    getParametersStub.returns(new Map());
    getTemplateBodyStub.returns('{"Resources": {}}');
    mockRos.updateStack.resolves({ body: {} });

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.be.false;
  });

  it('should handle error with empty data property', async () => {
    getParametersStub.returns(new Map());
    getTemplateBodyStub.returns('{"Resources": {}}');
    const error = new Error('Network error');
    (error as any).statusCode = 500;
    (error as any).data = { Message: '', RequestId: '' };
    mockRos.updateStack.rejects(error);

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.be.false;
    expect(stepErrorStub.calledWith('Failed to UpdateStack. Code: undefined, Message: , RequestId: ')).to.be.true;
  });

  it('should handle error without data property', async () => {
    getParametersStub.returns(new Map());
    getTemplateBodyStub.returns('{"Resources": {}}');
    const error = new Error('Network error');
    (error as any).statusCode = 500;
    (error as any).data = { Message: 'Network error', RequestId: 'test-id' };
    mockRos.updateStack.rejects(error);

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.be.false;
    expect(stepErrorStub.calledWith('Failed to UpdateStack. Code: undefined, Message: Network error, RequestId: test-id')).to.be.true;
  });

  it('should handle single parameter', async () => {
    const mockParameters = new Map([['Key1', 'Value1']]);
    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns('{"Resources": {}}');
    mockRos.updateStack.resolves({ body: { stackId: 'stack-456' } });

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.equal('stack-456');
    expect(stepInfoStub.calledWith('UpdateStack params is [\n  {\n    "parameterKey": "Key1",\n    "parameterValue": "Value1"\n  }\n]')).to.be.true;
  });

  it('should handle special characters in parameter values', async () => {
    const mockParameters = new Map([
      ['SpecialKey', 'value with spaces & symbols!@#'],
      ['EmptyValue', ''],
      ['NumberValue', '12345']
    ]);
    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns('{"Resources": {}}');
    mockRos.updateStack.resolves({ body: { stackId: 'stack-789' } });

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.equal('stack-789');
    const call = mockRos.updateStack.getCall(0);
    const request = call.args[0];
    expect(request.parameters).to.have.length(3);
  });

  it('should log parameters when provided', async () => {
    const mockParameters = new Map([['param1', 'value1']]);
    const mockTemplate = '{"Resources": {}}';
    
    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns(mockTemplate);
    mockRos.updateStack.resolves({ body: { stackId: 'stack-123' } });

    await updateStack(mockParams, mockRos, 'stack-12345');

    expect(stepInfoStub.calledWith('UpdateStack params is [\n  {\n    "parameterKey": "param1",\n    "parameterValue": "value1"\n  }\n]')).to.be.true;
  });

  it('should handle non-200 status codes with different error patterns', async () => {
    const mockParameters = new Map();
    const mockTemplate = '{"Resources": {}}';
    const mockError = {
      statusCode: 500,
      data: {
        Message: 'Internal server error'
      }
    };

    getParametersStub.returns(mockParameters);
    getTemplateBodyStub.returns(mockTemplate);
    mockRos.updateStack.rejects(mockError);

    const result = await updateStack(mockParams, mockRos, 'stack-12345');

    expect(result).to.be.false;
    expect(stepErrorStub.calledWith('Failed to UpdateStack. Code: undefined, Message: Internal server error, RequestId: undefined')).to.be.true;
  });
});